package net.sf.ivyide.idea;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
//import com.intellij.facet.*;
//import com.intellij.javaee.ejb.facet.EjbFacet;
//import com.intellij.javaee.ejb.facet.EjbFacetType;
//import com.intellij.javaee.ejb.facet.EjbFacetConfiguration;
//import com.intellij.util.descriptors.ConfigFileMetaData;
import fr.jayasoft.ivy.Artifact;
import fr.jayasoft.ivy.Ivy;
import fr.jayasoft.ivy.ModuleId;
import fr.jayasoft.ivy.ModuleRevisionId;
import net.sf.ivyide.ContentRoot;
import net.sf.ivyide.IvyEnvironmentInfo;
import net.sf.ivyide.ModuleSourceDescriptor;

import java.io.File;
import java.io.IOException;
import java.util.*;

public class UpdateModule
        implements ProgressAwareRunnable {

    private static final Logger LOGGER = Logger.getInstance(UpdateModule.class.getName());

    private IvyEnvironmentInfo m_environment;
    private LibraryTable m_projectLibraryTable;
    private Map<String, Library> m_usedProjectLibraries;
    private ModifiableRootModel m_rootModel;
    private ResolvedModule m_resolvedModule;


    public UpdateModule(IvyEnvironmentInfo settings, ResolvedModule resolvedModule, ModifiableRootModel rootModel, LibraryTable projectLibraryTable, Map<String, Library> usedProjectLibraries) {
        m_rootModel = rootModel;
        m_resolvedModule = resolvedModule;
        m_environment = settings;
        m_projectLibraryTable = projectLibraryTable;
        m_usedProjectLibraries = usedProjectLibraries;
    }

    private class LibraryContents {

        VirtualFile[] m_classes;
        private File[] m_sources;

        public LibraryContents(VirtualFile[] classes, File[] sources) {
            m_classes = classes;
            m_sources = sources;
        }

        public VirtualFile[] getClasses() {
            return m_classes;
        }

        public File[] getSources() {
            return m_sources;
        }
    }

    private interface LibraryContentsRetriever {
        LibraryContents getLibraryContents();
    }

    private class SimpleLibraryContentsRetriever implements LibraryContentsRetriever {
        private LibraryContents m_contents;

        public SimpleLibraryContentsRetriever(LibraryContents contents) {
            m_contents = contents;
        }

        public LibraryContents getLibraryContents() {
            return m_contents;
        }
    }

    public static Library synchronizeLib(final Map<String, Library> cachedLibraries, final LibraryTable libraryTable, final String name, LibraryContentsRetriever retriever, ProgressIndicator progressIndicator) {
        Library libraryByName = cachedLibraries.get(name);
        if (libraryByName == null) { //no yet synced (same artifact can be synced by different modules)
            final UpdateModule.LibraryContents libraryContents = retriever.getLibraryContents();

            libraryByName = ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareCallable<Library>() {
                public Library call(ProgressIndicator progressIndicator) throws Exception {
                    boolean modified = false;
                    Library libraryByName = libraryTable.getLibraryByName(name);
                    if (libraryByName == null) {
                        modified = true;
                        libraryByName = libraryTable.createLibrary(name);
                    }
                    cachedLibraries.put(name, libraryByName);
                    Library.ModifiableModel libModifiableModel = libraryByName.getModifiableModel();
                    modified = modified | UpdateModule.syncRoots(libraryContents.getClasses(), libModifiableModel, OrderRootType.CLASSES);
                    List<VirtualFile> sourceFiles = new LinkedList<VirtualFile>();
                    File[] sources = libraryContents.getSources();
                    if (sources != null) {
                        for (File source : sources) {
                            VirtualFile fileByIoFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(source);
                            if (fileByIoFile != null) {
                                sourceFiles.add(fileByIoFile);
                            }
                        }
                        modified = modified | UpdateModule.syncRoots(sourceFiles.toArray(new VirtualFile[sourceFiles.size()]), libModifiableModel, OrderRootType.SOURCES);
                    }
                    LOGGER.debug("Library modified " + name + ": " + modified);
                    if (modified) {
                        libModifiableModel.commit();
                    }
                    return libraryByName;
                }
            }, progressIndicator);

        }
        return libraryByName;

    }

    private static boolean syncRoots(VirtualFile[] classes, Library.ModifiableModel libModifiableModel, OrderRootType rootType) {
        boolean modified = false;
        List<VirtualFile> oldStuff = new LinkedList<VirtualFile>(Arrays.asList(libModifiableModel.getFiles(rootType)));
        for (VirtualFile aClass : classes) {
            boolean alreadyAvailable = false;
            for (VirtualFile virtualFile : oldStuff) {
                if (virtualFile.equals(aClass)) {
                    alreadyAvailable = true;
                    oldStuff.remove(virtualFile);
                    break;
                }
            }
            if (!alreadyAvailable) {
                modified = true;
                libModifiableModel.addRoot(aClass, rootType);
            }
        }
        //Remove old stuff that is not available in the new stuff
        for (VirtualFile virtualFile : oldStuff) {
            modified = true;
            UpdateModule.LOGGER.info("Removing " + virtualFile.getUrl() + " from " + libModifiableModel.getName());
            libModifiableModel.removeRoot(virtualFile.getUrl(), rootType);
        }
        return modified;
    }

    private ContentEntry getContentEntry(VirtualFile myContentRoot) {

        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            if (myContentRoot.equals(contentEntry.getFile())) {
                return contentEntry;
            }
        }
        return m_rootModel.addContentEntry(myContentRoot);
    }

    private void addSource(final Collection<SourceFolder> unusedSourceFolders, final Set<ContentEntry> unusedEntries, final ContentRoot[] contentRoots, ProgressIndicator progressIndicator) {
        //find a virtual file
        ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareRunnable() {
            public void run(ProgressIndicator progressIndicator) {

                for (ContentRoot contentRoot : contentRoots) {
                    VirtualFile virtualRootDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(contentRoot.getDir());
                    if (virtualRootDir == null) {
                        UpdateModule.LOGGER.error("Unexisting root: " + virtualRootDir);
                    } else {
                        ContentEntry contentEntry = getContentEntry(virtualRootDir);
                        unusedEntries.remove(contentEntry);
                        File[] mainJavaDirs = contentRoot.getMainJavaDirs();
                        for (File mainJavaDir : mainJavaDirs) {
                            syncSource(contentEntry, mainJavaDir, unusedSourceFolders, false);
                        }
                        File[] testJavaDirs = contentRoot.getTestJavaDirs();
                        for (File testJavaDir : testJavaDirs) {
                            syncSource(contentEntry, testJavaDir, unusedSourceFolders, true);
                        }
                        File[] excludedDirs = contentRoot.getExcludedDirs();
                        for (File excludedDir : excludedDirs) {
                            VirtualFile fileByIoFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(excludedDir);
                            if (fileByIoFile != null) {
                                contentEntry.addExcludeFolder(fileByIoFile);
                            }
                        }
                    }
                }
            }
        }, progressIndicator);
    }

    private void syncSource(ContentEntry contentEntry, File mainJavaDir, Collection<SourceFolder> unusedSourceFolders, boolean test) {
        VirtualFile fileByIoFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(mainJavaDir);
        if (fileByIoFile != null) {
            Iterator<SourceFolder> iterator = unusedSourceFolders.iterator();
            while (iterator.hasNext()) {
                SourceFolder sourceFolder = iterator.next();
                if (fileByIoFile.equals(sourceFolder.getFile())) {
                    if (!(sourceFolder.isTestSource() ^ test)) {
                        iterator.remove();
                        return;
                    }
                }

            }
            contentEntry.addSourceFolder(fileByIoFile, test);
        }
    }

    public void run(final ProgressIndicator progressIndicator) {
        try {
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId());
            progressIndicator.setText2("");
            progressIndicator.setText("Synchronizing module " + m_resolvedModule.getModuleId());
            final List<String> dependantModules = new LinkedList<String>();
            final Map<ModuleId, File> modules = m_environment.getModules();
            ModuleSourceDescriptor rootModuleDescriptor = m_environment.getModuleSource(modules.get(m_resolvedModule.getModuleId()));
            File compilerOutputPath = rootModuleDescriptor.getCompilerOutputPath();
            if (compilerOutputPath != null) {
                String path = compilerOutputPath.getPath();
                String url = VirtualFileManager.constructUrl("file", path.replace(File.separatorChar, '/'));
                m_rootModel.setCompilerOutputPath(url);
            }
            File compilerOutputPathForTests = rootModuleDescriptor.getCompilerOutputPathForTests();
            if (compilerOutputPathForTests != null) {
                m_rootModel.setCompilerOutputPathForTests(VirtualFileManager.constructUrl("file", compilerOutputPathForTests.getPath().replace(File.separatorChar, '/')));
            }
            m_rootModel.setExcludeOutput(true);
            
            final List<Library> librariesUseByThisModule = new LinkedList<Library>();

            Map<ModuleId, DependencyType> internalModules = m_resolvedModule.getInternalModules();
            final Set<SourceFolder> unusedSourceFolders = new HashSet<SourceFolder>();
            final Set<ContentEntry> unusedContentEntries = new HashSet<ContentEntry>();
            ContentEntry[] contentEntries = m_rootModel.getContentEntries();
            for (ContentEntry contentEntry : contentEntries) {
                unusedContentEntries.add(contentEntry);
                SourceFolder[] sourceFolders = contentEntry.getSourceFolders();
                unusedSourceFolders.addAll(Arrays.asList(sourceFolders));
            }

            addModuleSource(librariesUseByThisModule, unusedSourceFolders, unusedContentEntries, rootModuleDescriptor, progressIndicator);

            final Map<ModuleRevisionId, List<Artifact>> moduleArtifacts = m_resolvedModule.getModuleArtifacts();
            Set<ModuleRevisionId> moduleRevisionIds = moduleArtifacts.keySet();
            UpdateModule.LOGGER.debug("ModuleArtifacts " + moduleArtifacts);
            for (final ModuleRevisionId moduleRevisionId : moduleRevisionIds) {
                DependencyType dependencyType = internalModules.get(moduleRevisionId.getModuleId());
                UpdateModule.LOGGER.debug("DependencyTYpe for " + moduleRevisionId + " " + dependencyType);
                if (dependencyType == null || dependencyType.equals(DependencyType.LIBRARY)) {
                    String libName = (new StringBuilder()).append(moduleRevisionId.getName()).append("-").append(moduleRevisionId.getRevision()).toString();
                    LibraryContentsRetriever retriever = new LibraryContentsRetriever() {
                        public LibraryContents getLibraryContents() {
                            List<VirtualFile> libDirs = new LinkedList<VirtualFile>();
                            List<Artifact> artifacts = moduleArtifacts.get(moduleRevisionId);
                            UpdateModule.LOGGER.debug("Artifacts for " + moduleRevisionId + ": " + artifacts.size());
                            for (Artifact artifact : artifacts) {
                                Ivy ivy = m_environment.initializeIvy();
                                final File jarFile = ivy.getArchiveFileInCache(ivy.getDefaultCache(), artifact);
                                VirtualFile jarVirtualFile = ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareCallable<VirtualFile>() {
                                    public VirtualFile call(ProgressIndicator progressIndicator) throws Exception {
                                        UpdateModule.LOGGER.debug("Got jar file from ivy cache: " + jarFile);
                                        try {
                                            File canonicalJarFile = jarFile.getCanonicalFile();
                                            String urlString = VirtualFileManager.constructUrl("jar", (new StringBuilder()).append(canonicalJarFile.getAbsolutePath().replaceAll("\\\\", "/")).append("!/").toString());
                                            return VirtualFileManager.getInstance().refreshAndFindFileByUrl(urlString);
                                        } catch (IOException e) {
                                            throw new RuntimeException(e);
                                        }
                                    }
                                }, progressIndicator);
                                libDirs.add(jarVirtualFile);
                                UpdateModule.LOGGER.debug("Added artifact " + artifact.getName() + " " + jarVirtualFile.getPath());
                            }
                            File[] sources = null;
                            File moduleSourceDescriptor = modules.get(moduleRevisionId.getModuleId());
                            if (moduleSourceDescriptor != null) {
                                List<File> sourceFiles = new LinkedList<File>();
                                progressIndicator.setText2("Updating library " + moduleRevisionId.getModuleId());
                                ContentRoot[] contentRoots = m_environment.getModuleSource(moduleSourceDescriptor).getContentRoots();
                                for (ContentRoot contentRoot : contentRoots) {
                                    sourceFiles.addAll(Arrays.asList(contentRoot.getMainJavaDirs()));
                                    sourceFiles.addAll(Arrays.asList(contentRoot.getTestJavaDirs()));
                                }
                                sources = sourceFiles.toArray(new File[sourceFiles.size()]);
                            }
                            UpdateModule.LOGGER.debug("Returning library contents");
                            return new LibraryContents(libDirs.toArray(new VirtualFile[libDirs.size()]), sources);
                        }
                    };
                    Library library = UpdateModule.synchronizeLib(m_usedProjectLibraries, m_projectLibraryTable, libName, retriever, progressIndicator);
                    librariesUseByThisModule.add(library);

                } else {
                    String dependantModuleName = moduleRevisionId.getModuleId().getName();
                    m_rootModel.addInvalidModuleEntry(dependantModuleName);
                    dependantModules.add(dependantModuleName);
                    //ModuleSourceDescriptor moduleSourceDescriptor = m_environment.getModuleSource(file);
                    //addModuleSource(librariesUseByThisModule, unusedSourceFolders, unusedContentEntries, moduleSourceDescriptor);
                }
            }
            /*
            FacetManager facetManager = FacetManager.getInstance(m_rootModel.getModule());


            ModifiableFacetModel facetModel = facetManager.createModifiableModel();
            EjbFacetConfiguration ejbFacetConfiguration = EjbFacetType.INSTANCE.createDefaultConfiguration();
            ConfigFileMetaData metaData =new ConfigFileMetaData(); 
            ejbFacetConfiguration.getDescriptorsConfiguration().getConfigFileInfo()
            FacetType<EjbFacet, FacetConfiguration> facetType = FacetTypeRegistry.getInstance().findFacetType(EjbFacet.ID);
            FacetConfiguration configuration = facetType.createDefaultConfiguration();

            EjbFacet ejbFacet = facetType.createFacet(m_rootModel.getModule(), "IvyEJB", configuration, null);
            ejbFacet.getEjbJarDescriptor().;

            facetModel.addFacet(ejbFacet);
            facetModel.commit();
            */
            progressIndicator.setText2("Cleaning unused libraries");
            ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareRunnable() {
                public void run(ProgressIndicator progressIndicator) {
                    synchronizeUsedLibraries(librariesUseByThisModule, m_rootModel.getModuleLibraryTable(), m_rootModel, true);
                    synchronizeUsedLibraries(librariesUseByThisModule, m_projectLibraryTable, m_rootModel, false);
                    cleanUnusedLibraries(librariesUseByThisModule, m_rootModel.getModuleLibraryTable());
                    cleanUnusedSources(unusedSourceFolders, unusedContentEntries);
                    //remove unused module dependencies
                    for (OrderEntry orderEntry : m_rootModel.getOrderEntries()) {
                        if (orderEntry instanceof ModuleOrderEntry) {
                            String dependantModuleName = ((ModuleOrderEntry) orderEntry).getModuleName();
                            if (!dependantModules.contains(dependantModuleName)) {
                                m_rootModel.removeOrderEntry(orderEntry);
                            }
                        } else if (orderEntry instanceof LibraryOrderEntry) {
                            LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry) orderEntry;
                            if (!librariesUseByThisModule.contains(libraryOrderEntry.getLibrary())) {
                                m_rootModel.removeOrderEntry(orderEntry);
                            }
                        }
                    }
                }
            }, progressIndicator);
            progressIndicator.setText2("");
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId() + " done");
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void cleanUnusedSources(Collection<SourceFolder> unusedSourceFolders, Set<ContentEntry> unusedEntries) {
        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            for (SourceFolder sourceFolder : unusedSourceFolders) {
                if (contentEntry.equals(sourceFolder.getContentEntry())) {
                    contentEntry.removeSourceFolder(sourceFolder);
                }
            }
            if (unusedEntries.contains(contentEntry)) {
                m_rootModel.removeContentEntry(contentEntry);
            }
        }
    }

    private void synchronizeUsedLibraries(final List<Library> usedLibraries, final LibraryTable projectLibraryTable, final ModifiableRootModel rootModel, final boolean export) {
        //for all entries in the model that are not used, remove order entry..
        Library[] libraryIterator = projectLibraryTable.getLibraries();
        for (Library library : libraryIterator) {
            if (!usedLibraries.contains(library)) {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry != null) {
                    rootModel.removeOrderEntry(libraryOrderEntry);
                }
            } else {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry == null) {
                    libraryOrderEntry = rootModel.addLibraryEntry(library);
                }
                libraryOrderEntry.setExported(export);
            }
        }
    }

    static void cleanUnusedLibraries(Collection<Library> usedLibraries, LibraryTable libraryTable) {
        Library[] libraries = libraryTable.getLibraries();
        for (Library library : libraries) {
            if (!usedLibraries.contains(library)) {
                //remove
                LOGGER.debug("Removing unused library: " + library.getName() + " " + library);
                libraryTable.removeLibrary(library);
            }
        }
    }


    private void addModuleSource(List<Library> used, Collection<SourceFolder> unusedSourceFolders, Set<ContentEntry> unusedEntries, ModuleSourceDescriptor rootModuleDescriptor, ProgressIndicator progressIndicator) {
        ContentRoot[] contentRoots = rootModuleDescriptor.getContentRoots();
        addSource(unusedSourceFolders, unusedEntries, contentRoots, progressIndicator);

        LibraryTable libraryTable = m_rootModel.getModuleLibraryTable();

        for (ContentRoot contentRoot : contentRoots) {
            final List<VirtualFile> virtualFiles = new LinkedList<VirtualFile>();
            final List<File> libDirs = new LinkedList<File>();
            libDirs.addAll(Arrays.asList(contentRoot.getMainResourceDirs()));
            libDirs.addAll(Arrays.asList(contentRoot.getTestResourceDirs()));
            if (libDirs.size() > 0) {
                ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareRunnable() {
                    public void run(ProgressIndicator progressIndicator) {
                        for (File file : libDirs) {
                            VirtualFile fileByIoFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
                            if (fileByIoFile != null) {
                                virtualFiles.add(fileByIoFile);
                            } else {
                                LOGGER.error("Unknown file: " + file);
                            }
                        }
                    }
                }, progressIndicator);
                used.add(UpdateModule.synchronizeLib(new HashMap<String, Library>(), libraryTable, contentRoot.getDir().getPath(), new SimpleLibraryContentsRetriever(new LibraryContents(virtualFiles.toArray(new VirtualFile[virtualFiles.size()]), null)), progressIndicator));
            }
        }
    }

}
